/*
 * Copyright (c) 2010 by Aleksey Zhukov
 * Licensed under the Apache License, Version 2.0 (the "License")
 */

package ru.listfunc4j;

import javassist.*;

import java.lang.reflect.Method;
import java.util.*;

/**
 * User: J0k3r
 * Date: 23.10.2010
 * Time: 21:15:58
 */
@SuppressWarnings({"UnusedDeclaration"})
public class ListFunction {
    private Iterable currentIterable;

    private static ClassPool pool = ClassPool.getDefault();

    static {
        try {
            LibraryWork.initilizeLibrary();
            AgentWork.init();
        } catch (Exception ex) { ex.printStackTrace(); }
    }

    public static void init() {/* just call static initializer */}

    private static String getInvokerClass() {
        return Thread.currentThread().getStackTrace()[3].getClassName();
    }

    /**
     * Initialisation of new block of lambda-expressions for collections
     * @param iterable collection
     * @return collection worker
     */
    public static ListFunction forEach(Iterable iterable) {
        return forEach(iterable, getInvokerClass());
    }

    /**
     * Initialisation of new block of lambda-expressions for collections
     * @param iterable collection
     * @param invokerClass where method was invoked from
     * @return collection worker
     */
    private static ListFunction forEach(Iterable iterable, String invokerClass) {
        try {
            CtClass[] classes = pool.get(invokerClass).getNestedClasses();
            for (CtClass lambda : classes) {
                if (lambda.getSuperclass().getName().equals(Lambda.class.getName()) ||
                        lambda.getSuperclass().getName().equals(Updater.class.getName()) ||
                        lambda.getSuperclass().getName().equals(Sorter.class.getName()) ||
                        lambda.getSuperclass().getName().equals(Filter.class.getName()) ||
                        lambda.getSuperclass().getName().equals(Aggregator.class.getName())) {
                    CtClass current = lambda;
                    if (lambda.getAttribute("modified") == null) {
                        current = modifyInitializer(lambda);
                    }
                    AgentWork.redefineClass(Class.forName(lambda.getName()), current.toBytecode());
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        return new ListFunction(iterable);
    }

    public ListFunction(Iterable iterable) {
        this.currentIterable = iterable;
    }

    public ListFunction sort(Sorter sorter) {
        return this.add(sorter);
    }

    public ListFunction aggregate(Aggregator aggregator) {
        return this.add(aggregator);
    }

    public ListFunction filter(Filter filter) {
        return this.add(filter);
    }

    public ListFunction update(Updater updater) {
        return this.add(updater);
    }

    @SuppressWarnings({"unchecked"})
    public ListFunction add(Lambda lambda) {
        try {
            Class aClass = modifyClass(lambda);
            Lambda newLambda = (Lambda) aClass.getDeclaredConstructors()[0].newInstance(lambda.params);
            this.exec(newLambda, lambda.params);
            return this;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public static CtClass modifyInitializer(CtClass aClass) {
        try {
            CtConstructor ctConstructor = aClass.getConstructors()[0];
            ctConstructor.insertBeforeBody("if (!active) return;");
            ctConstructor.insertAfter("this.params = $args;");
            aClass.setAttribute("modified", new byte[]{1});
            return aClass;
        } catch (Exception ex1) {
            throw new RuntimeException(ex1);
        }
    }

    public static CtClass modifyClass(CtClass aClass) {
        try {
            if (aClass.getAttribute("modified") == null) {
                aClass.setAttribute("modified", new byte[]{1});
                aClass.setModifiers(Modifier.PUBLIC);
                CtConstructor ctConstructor = aClass.getConstructors()[0];
                ctConstructor.setModifiers(Modifier.PUBLIC);
                CtMethod testMethod = ctConstructor.toMethod("invoke", aClass);
                testMethod.setModifiers(Modifier.PUBLIC);
                ctConstructor.insertBeforeBody("if (!super.active) return;");
                aClass.addMethod(testMethod);
                return aClass;
            }
        } catch (Exception ex1) {

            throw new RuntimeException(ex1);
        }
        return null;
    }

    public static Class modifyClass(Object lambda) {
        try {
            try {
                return Class.forName(lambda.getClass().getName() + "_");
            } catch (ClassNotFoundException ex) {
                CtClass modifiedClass = modifyClass(pool.getAndRename(lambda.getClass().getName(),
                        lambda.getClass().getName() + "_"));
                Class aClass = modifiedClass.toClass(ClassLoader.getSystemClassLoader(), null);
                modifiedClass.defrost();
                return aClass;
            }
        } catch (Exception ex1) {
            throw new RuntimeException(ex1);
        }
    }

    @SuppressWarnings({"unchecked"})
    protected ListFunction exec(final Lambda lambda, final Object[] params) {
        try {
            Class[] classes = new Class[params.length];
            for (int i = 0; i < params.length; i++)
                classes[i] = params[i].getClass();
            final Method method = lambda.getClass().getDeclaredMethod("invoke", classes);
            Iterator curIterator = currentIterable.iterator();
            if (lambda instanceof Sorter) {
                Object[] array = this.toArray(Object[].class);
                Arrays.sort(array, new Comparator<Object>() {
                    @Override public int compare(Object o1, Object o2) {
                        // setting up comparable objects
                        ((Sorter)lambda).set$1(o1);
                        ((Sorter)lambda).set$2(o2);
                        try {
                            method.invoke(lambda, params);
                            return (Integer) lambda._;
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
                this.currentIterable = Arrays.asList(array);
            } else if (lambda instanceof Updater) {
                // no need to make new currentIterable - just process items..
                while (curIterator.hasNext()) {
                    Object currentValue = curIterator.next();
                    lambda.setCurrent(currentValue);
                    method.invoke(lambda, params);
                }
            } else {
                LinkedList nextIterable = new LinkedList();
                while (curIterator.hasNext()) {
                    Object currentValue = curIterator.next();
                    lambda.setCurrent(currentValue);
                    if (lambda instanceof Aggregator) {
                        method.invoke(lambda, params);
                    } else {
                        method.invoke(lambda, params);
                        if (lambda instanceof Filter) {
                            if ((Boolean)lambda.getValue())
                                nextIterable.add(currentValue);
                        } else
                            nextIterable.add(lambda.getValue());
                    }
                }
                if (lambda instanceof Aggregator)
                    nextIterable.add(lambda.getValue());
                this.currentIterable = nextIterable;
            }
            return this;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    @SuppressWarnings({"unchecked"})
    public < T > List < T > toList() {
        List list;
        if (currentIterable instanceof List)
            list = (List)currentIterable;
        else {
            list = new LinkedList();
            for (Object o : currentIterable) list.add(o);
        }
        return (List < T >) list;
    }

    @SuppressWarnings("unchecked")
        public < T > T[] toArray(Class<? extends T[]> clazz) {
        List list = toList();
        return Arrays.copyOf(list.toArray(new Object[list.size()]), list.size(), clazz);
    }

    @SuppressWarnings({"unchecked"})
    public < T > T uniqueResult() {
        return (T) this.currentIterable.iterator().next();
    }

    @SuppressWarnings({"unchecked", "ConstantConditions"})
    public <T> T of(Class<? extends T > clazz) throws ListFunctionException {
        CtClass newClass = (pool.getOrNull(clazz.getName() + "_"));
        Class aClass = null;
        try {
            if (newClass != null)
                aClass = Class.forName(clazz.getName() + "_");
            else {
                CtClass superClass = pool.get(clazz.getName());
                newClass = pool.makeClass(clazz.getName() + "_");
                newClass.setSuperclass(superClass);
                // adding field
                newClass.addField(CtField.make("public " +  ListFunction.class.getName() + " $listFunction;", newClass));
                // adding constructor with initializer of just created field
                for (CtConstructor superConstructor : superClass.getConstructors()) {
                    if (superConstructor.getParameterTypes().length == 0) {
                        newClass.addConstructor(CtNewConstructor.make(superConstructor.getParameterTypes(), superConstructor.getExceptionTypes(), "{}", newClass));
                        newClass.addConstructor(CtNewConstructor.make(new CtClass[] {pool.get(ListFunction.class.getName())},
                                new CtClass[] {}, "this.$listFunction = $1;", newClass));
                        CtMethod[] methods = superClass.getDeclaredMethods();
                        for (CtMethod method : methods) {
                            if (((method.getModifiers() & Modifier.FINAL) > 0) ||
                                    ((method.getModifiers() & Modifier.ABSTRACT) > 0))
                                continue;
                            StringBuffer params = new StringBuffer();
                            for (int nI = 1; nI <= method.getParameterTypes().length; ++nI)
                                params.append(params.length() > 0 ? ", " : "").append("$").append(nI);
                            newClass.addMethod(CtNewMethod.make(method.getReturnType(),
                                    method.getName(), method.getParameterTypes(), method.getExceptionTypes(),
                                    "{Iterable iterable = $listFunction.getCurrentIterable();\n" +
                                            "java.util.Iterator i$ = iterable.iterator();" +
                                            "while (i$.hasNext()) ((" + clazz.getName() + ") i$.next())." + method.getName() + "(" + params.toString() + ");\n" +
                                            (method.getReturnType().equals(pool.get("void")) ?
                                                    "}" : "return " + getReturnType(method.getReturnType()) + ";}"), newClass));
                        }
                        aClass = newClass.toClass();
                    }
                }
            }
        } catch (Exception ex) {
            throw new ListFunctionException(ex);
        }
        try {
            return (T) aClass.getDeclaredConstructor(ListFunction.class).newInstance(this);
        } catch (Exception ex) {
            throw new ListFunctionException("No constructor in class " + clazz.getName() + " without parameters");
        }
    }

    private String getReturnType(CtClass returnType) throws NotFoundException {
        if (returnType.isPrimitive()) {
            if (returnType.equals(pool.get("int"))) return "0";
            else if (returnType.equals(pool.get("long"))) return "0L";
            else if (returnType.equals(pool.get("double"))) return "0D";
            else if (returnType.equals(pool.get("float"))) return "0F";
            else if (returnType.equals(pool.get("boolean"))) return "false";
        }
        return "null";
    }

    public Iterable getCurrentIterable() {
        return currentIterable;
    }
}
